﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Core.Log;
using Core.Rendering.Buffer;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;

namespace Core.Rendering.Resources
{
    public class Shader
    {
        public int Id { get; private set; }
        public string? Path { get; private set; }
        private Dictionary<string, int> _cache = new Dictionary<string, int>();
        public List<UniformInfo> UniformInfos { get; private set; } = new List<UniformInfo>();
        public bool IsDestroy { get; private set; } = false;

        public static Shader Create(string path) => new Shader(path);
        public static Shader Create(string vertexSource, string fragmentSource) => new Shader(vertexSource, fragmentSource);
        public void ReCompile()
        {
            if(Path != null)
            {
                GL.DeleteProgram(Id);
                (string vertexSource, string fragmentSource) = LoadShaderFromDisk(Path);
                Id = CreateProgram(vertexSource, fragmentSource);
                QueryUniform();
            }
        }
        public void SetUniform(string name, int value) => GL.Uniform1(GetUniformLocation(name), value);
        public void SetUniform(string name, float value) => GL.Uniform1(GetUniformLocation(name), value);
        public void SetUniform(string name, Vector2 value) => GL.Uniform2(GetUniformLocation(name), value);
        public void SetUniform(string name, Vector3 value) => GL.Uniform3(GetUniformLocation(name), value);
        public void SetUniform(string name, Vector4 value) => GL.Uniform4(GetUniformLocation(name), value);
        public void SetUniform(string name, Matrix2 value) => GL.UniformMatrix2(GetUniformLocation(name), false, ref value);
        public void SetUniform(string name, Matrix2x3 value) => GL.UniformMatrix2x3(GetUniformLocation(name), false, ref value);
        public void SetUniform(string name, Matrix2x4 value) => GL.UniformMatrix2x4(GetUniformLocation(name), false, ref value);
        public void SetUniform(string name, Matrix3 value) => GL.UniformMatrix3(GetUniformLocation(name), false, ref value);
        public void SetUniform(string name, Matrix4 value) => GL.UniformMatrix4(GetUniformLocation(name), false, ref value);
        public void GetUniform(string name, out int v)=> GL.GetUniform(Id, GetUniformLocation(name), out v);
        public void GetUniform(string name, out float v) => GL.GetUniform(Id, GetUniformLocation(name), out v);
        public void GetUniform(string name,out Vector2 v)
        {
            float[]res = new float[2];
            GL.GetUniform(Id, GetUniformLocation(name), res);
            v = new Vector2(res[0], res[1]);
        }
        public void GetUniform(string name, out Vector3 v)
        {
            float[] res = new float[3];
            GL.GetUniform(Id, GetUniformLocation(name), res);
            v = new Vector3(res[0], res[1], res[2]);
        }
        public void GetUniform(string name, out Vector4 v)
        {
            float[] res = new float[4];
            GL.GetUniform(Id, GetUniformLocation(name), res);
            v = new Vector4(res[0], res[1], res[2], res[3]);
        }
        public void GetUniform(string name, out Matrix3 v)
        {
            float[] res = new float[9];
            GL.GetUniform(Id, GetUniformLocation(name), res);
            v = new Matrix3(res[0], res[1], res[2], res[3], res[4], res[5], res[6], res[7], res[8]);
        }
        public void GetUniform(string name, out Matrix4 v)
        {
            float[] res = new float[16];
            GL.GetUniform(Id, GetUniformLocation(name), res);
            v = new Matrix4(res[0], res[1], res[2], res[3], res[4], res[5], res[6], res[7], res[8],
                res[9], res[10], res[11], res[12], res[13], res[14], res[15]);
        }
        public void QueryUniform()
        {
            UniformInfos.Clear();
            GL.GetProgram(Id, GetProgramParameterName.ActiveUniforms,out int uniformCount);
            for(int i = 0;i< uniformCount; i++)
            {
                GL.GetActiveUniform(Id,i,1024,out _,out _,out ActiveUniformType uniformType,out string name);
                object? value = null;
                switch(uniformType)
                {
                    case ActiveUniformType.Bool:
                        GetUniform(name, out int v);
                        value = v;
                        break;
                    case ActiveUniformType.Int:
                        GetUniform(name, out int v1);
                        value = v1;
                        break;
                    case ActiveUniformType.Float:
                        GetUniform(name, out float v2);
                        value = v2;
                        break;
                    case ActiveUniformType.FloatVec2:
                        GetUniform(name, out Vector2 v3);
                        value = v3;
                        break;
                    case ActiveUniformType.FloatVec3:
                        GetUniform(name, out Vector3 v4);
                        value = v4;
                        break;
                    case ActiveUniformType.FloatVec4:
                        GetUniform(name, out Vector4 v5);
                        value = v5;
                        break;
                    case ActiveUniformType.FloatMat3:
                        GetUniform(name, out Matrix3 v6);
                        value = v6;
                        break;
                    case ActiveUniformType.FloatMat4:
                        GetUniform(name, out Matrix4 v7);
                        value = v7;
                        break;
                }
                UniformInfos.Add(new UniformInfo(name,GetUniformLocation(name),uniformType, value));
            }
        }
        public void Bind()
        {
            GL.UseProgram(Id);
        }
        public static void UnBind()
        {
            GL.UseProgram(0);
        }
        private Shader(string path)
        {
            Path = path;
            (string vertexSource, string fragmentSource) = LoadShaderFromDisk(path);
            Id = CreateProgram(vertexSource, fragmentSource);
            QueryUniform();
            UniformBufferObject.BindingPointToShader(this, "EngineUbo", 0);
        }
        private Shader(string vertexSource, string fragmentSource)
        {
            Id = CreateProgram(vertexSource, fragmentSource);
            QueryUniform();
        }
        private int GetUniformLocation(string name)
        {
            if (_cache.ContainsKey(name)) return _cache[name];
            int location = GL.GetUniformLocation(Id, name);
            _cache.Add(name, location);
            return location;
        }
        private static int CreateProgram(string vertexShaderSource, string fragmentShaderSource)
        {
            int vertexShader = CreateShader(ShaderType.VertexShader, vertexShaderSource);
            int fragmentShader = CreateShader(ShaderType.FragmentShader, fragmentShaderSource);
            int id = GL.CreateProgram();
            GL.AttachShader(id, vertexShader);
            GL.AttachShader(id, fragmentShader);
            GL.LinkProgram(id);
            GL.GetProgram(id, GetProgramParameterName.LinkStatus, out int success);
            if (success == 0)
            {
                GL.GetProgramInfoLog(id, out string info);
                ExLog.ErrorLogCore(info);
            }
            GL.DeleteShader(vertexShader);
            GL.DeleteShader(fragmentShader);
            return id;
        }
        private (string vertexSource, string fragmentSource) LoadShaderFromDisk(string path)
        {
            string[] lines = File.ReadAllLines(path);
            int vertexIndex = Array.IndexOf(lines, "#shader vertex");
            int fragmentIndex = Array.IndexOf(lines, "#shader fragment");
            string[] vertexLines = lines.Skip(vertexIndex + 1).Take(fragmentIndex - 1).ToArray();
            string[] fragmentLines = lines.Skip(fragmentIndex + 1).ToArray();
            string vertexShader = string.Join("\n", vertexLines);
            string fragmentShader = string.Join("\n", fragmentLines);
            return (vertexShader, fragmentShader);
        }
        private static int CreateShader(ShaderType type, string source)
        {
            int id = GL.CreateShader(type);
            GL.ShaderSource(id, source);
            GL.CompileShader(id);
            GL.GetShader(id, ShaderParameter.CompileStatus, out int success);
            if (success == 0)
            {
                GL.GetShaderInfoLog(id, out string info);
                ExLog.ErrorLogCore(type.ToString() + "\n" + info);
                Console.WriteLine("error:" + type.ToString());
                Console.WriteLine(info);
            }
            return id;
        }
        private void ReleaseUnmanageResources()
        {
            GL.DeleteProgram(Id);
        }
        public void Dispose()
        {
            if (IsDestroy) return;
            IsDestroy = true;
            ReleaseUnmanageResources();
            GC.SuppressFinalize(this);
        }
        ~Shader()
        {
            ReleaseUnmanageResources();
        }
    }
}
